package org.koin.core.scope;

import org.koin.core.Koin;
import org.koin.core.annotation.KoinInternalApi;
import org.koin.core.definition.BeanDefinition;
import org.koin.core.error.*;
import org.koin.core.logger.Level;
import org.koin.core.logger.Logger;
import org.koin.core.parameter.DefinitionParameters;
import org.koin.core.parameter.ParametersDefinition;
import org.koin.core.qualifier.Qualifier;
import org.koin.core.registry.InstanceRegistry;
import org.koin.ext.ClassUtil;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.function.Consumer;

@KoinInternalApi
public class Scope {

    private String id;
    private ScopeDefinition _scopeDefinition;
    private Koin _koin;
    private ArrayList<Scope> linkedScopes = new ArrayList<>();
    private InstanceRegistry instanceRegistry;
    private Object _source = null;
    private List<ScopeCallback> _callbacks = new ArrayList<>();
    private boolean _closed = false;
    private DefinitionParameters _parameters = null;
    private Logger logger;

    public Scope(String id, @KoinInternalApi ScopeDefinition _scopeDefinition, Koin _koin) {
        this.id = id;
        this._scopeDefinition = _scopeDefinition;
        this._koin = _koin;
        logger = _koin.getLogger();
        instanceRegistry = new InstanceRegistry(_koin, this);
    }

    public void create(List<Scope> links) {
        instanceRegistry.create(_scopeDefinition.getDefinitions());
        linkedScopes.addAll(links);
    }

    public <T> T getSource(Class<T> clazz) {
        if (_source != null && _source.getClass().equals(clazz)) {
            return (T) _source;
        } else {
            throw new IllegalStateException(
                    "Can't use Scope source for "
                            + ClassUtil.getFullName(clazz) + " - source is:" + _source
            );
        }
    }

    @KoinInternalApi
    public void setSource(Object t) {
        _source = t;
    }

    /**
     * Add parent Scopes to allow instance resolution
     * i.e: linkTo(scopeC) - allow to resolve instance to current scope and scopeC
     *
     * @param scopes - Scopes to link with
     */
    public void linkTo(Scope... scopes) {
        if (!_scopeDefinition.isRoot()) {
            linkedScopes.addAll(Arrays.asList(scopes));
        } else {
            throw new IllegalStateException("Can't add scope link to a root scope");
        }
    }

    public void unlink(Scope... scopes) {
        if (!_scopeDefinition.isRoot()) {
            linkedScopes.removeAll(Arrays.asList(scopes));
        } else {
            throw new IllegalStateException("Can't remove scope link to a root scope");
        }
    }


    public <T> T inject(Class clazz) throws DefinitionParameterException, NoBeanDefFoundException, ClosedScopeException {
        return get(clazz);
    }

    public <T> T inject(Class clazz, Qualifier qualifier) throws DefinitionParameterException, NoBeanDefFoundException, ClosedScopeException {
        return get(clazz, qualifier);
    }

    public <T> T inject(Class clazz, ParametersDefinition parameters) throws DefinitionParameterException, NoBeanDefFoundException, ClosedScopeException {
        return get(clazz, parameters);
    }

    public <T> T inject(Class clazz,
                        Qualifier qualifier,
                        ParametersDefinition parameters) throws DefinitionParameterException, NoBeanDefFoundException, ClosedScopeException {
        return get(clazz, qualifier, parameters);
    }


    public <T> T injectOrNull(Class clazz) {
        return getOrNull(clazz);
    }

    public <T> T getOrNull(
            Class clazz,
            Qualifier qualifier,
            ParametersDefinition parameters
    ) {
        try {
            return get(clazz, qualifier, parameters);
        } catch (ClosedScopeException e) {
            _koin.getLogger().debug("Koin.getOrNull - scope closed - no instance found for "
                    + ClassUtil.getFullName(clazz) + " on scope " + toString());
        } catch (DefinitionParameterException e) {
            _koin.getLogger().debug("Koin.getOrNull - definition parameter - no instance found for "
                    + ClassUtil.getFullName(clazz) + " on scope " + toString());
        } catch (NoBeanDefFoundException e) {
            _koin.getLogger().debug("Koin.getOrNull - no instance found for " + ClassUtil.getFullName(clazz)
                    + " on scope " + toString());
        }
        return null;
    }

    public <T> T getOrNull(
            Class clazz
    ) {

        return this.getOrNull(clazz, null, null);
    }

    /**
     * Get a Koin instance
     *
     * @param qualifier
     * @param parameters
     */
    public <T> T get(
            Class clazz,
            Qualifier qualifier,
            ParametersDefinition parameters
    ) throws ClosedScopeException, DefinitionParameterException, NoBeanDefFoundException {
        if (_koin.getLogger().isAt(Level.DEBUG)) {
            String qualifierString = "";

            if (qualifier != null) {
                qualifierString = " with qualifier '" + qualifier + "'";
            }

            _koin.getLogger().debug("+- '" + ClassUtil.getFullName(clazz)
                    + "'" + qualifierString + "");
            double start = System.nanoTime() / 1000000.0;
            T instance = resolveInstance(qualifier, clazz, parameters);
            double end = System.nanoTime() / 1000000.0;
            DecimalFormat to = new DecimalFormat("0.0000");
            String duration = to.format(end - start);
            _koin.getLogger().debug("|- '" + ClassUtil.getFullName(clazz)
                    + "' in " + duration + " ms");
            return instance;
        } else {
            return resolveInstance(qualifier, clazz, parameters);
        }
    }

    /**
     * Get a Koin instance
     *
     * @param parameters
     */
    public <T> T get(
            Class clazz,
            ParametersDefinition parameters
    ) throws ClosedScopeException, DefinitionParameterException, NoBeanDefFoundException {
        return get(clazz, null, parameters);
    }

    /**
     * Get a Koin instance
     *
     * @param qualifier
     */
    public <T> T get(
            Class clazz,
            Qualifier qualifier
    ) throws ClosedScopeException, DefinitionParameterException, NoBeanDefFoundException {
        return get(clazz, qualifier, null);
    }

    public <T> T get(
            Class clazz
    ) throws ClosedScopeException, DefinitionParameterException, NoBeanDefFoundException {
        return get(clazz, null, null);
    }

    private <T> T resolveInstance(
            Qualifier qualifier,
            Class clazz,
            ParametersDefinition parameters
    ) throws ClosedScopeException, DefinitionParameterException, NoBeanDefFoundException {
        if (_closed) {
            throw new ClosedScopeException("Scope '" + id + "' is closed");
        }
        String indexKey = BeanDefinition.indexKey(clazz, qualifier);
        T result = instanceRegistry.resolveInstance(indexKey, parameters);
        if (result == null) {
            _koin.getLogger().debug("'" + ClassUtil.getFullName(clazz) + "' - q:'"
                    + qualifier + "' not found in current scope");
            result = getFromSource(clazz);
        }
        if (result == null) {
            _koin.getLogger().debug("'" + ClassUtil.getFullName(clazz) + "' - q:'"
                    + qualifier + "' not found in current scope's source");
            if (_parameters != null) {
                result = _parameters.getOrNull(clazz);
            }
        }
        if (result == null) {
            _koin.getLogger().debug("'" + ClassUtil.getFullName(clazz) + "' - q:'"
                    + qualifier + "' not found in injected parameters");
            result = findInOtherScope(clazz, qualifier, parameters);
        }
        if (result == null) {
            _koin.getLogger().debug("'" + ClassUtil.getFullName(clazz) + "' - q:'"
                    + qualifier + "' not found in linked scopes");
            throwDefinitionNotFound(qualifier, clazz);
            return null;
        } else {
            return result;
        }
    }

    private <T> T getFromSource(Class clazz) {
        if (clazz.isInstance(_source)) {
            if (_source != null) {
                return (T) _source;
            }
        }
        return null;
    }

    private <T> T findInOtherScope(
            Class clazz,
            Qualifier qualifier,
            ParametersDefinition parameters
    ) {
        T instance = null;
        for (Scope scope : linkedScopes) {
            instance = scope.getOrNull(
                    clazz,
                    qualifier,
                    parameters);
            if (instance != null) {
                break;
            }
        }
        return instance;
    }

    private void throwDefinitionNotFound(
            Qualifier qualifier,
            Class clazz
    ) throws NoBeanDefFoundException {
        String qualifierString = "";
        if (qualifier != null) {
            qualifierString = " & qualifier:'" + qualifier + "'";
        }
        throw new NoBeanDefFoundException(
                "No definition found for class:'" + ClassUtil.getFullName(clazz) + "'"
                        + qualifierString + ". Check your definitions!"
        );
    }

    public void createEagerInstances() {
        if (_scopeDefinition.isRoot()) {
            instanceRegistry.createEagerInstances();
        }
    }

    /**
     * Declare a component definition from the given instance
     * This result of declaring a scoped/single definition of type T, returning the given instance
     * (single definition of the current scope is root)
     *
     * @param instance       The instance you're declaring.
     * @param qualifier      Qualifier for this declaration
     * @param secondaryTypes List of secondary bound types
     * @param override       Allows to override a previous declaration of the
     *                       same type (default to false).
     */
    public <T> void declare(
            Class<T> clazz,
            T instance,
            Qualifier qualifier,
            List<Class> secondaryTypes,
            boolean override
    ) throws DefinitionOverrideException {
        synchronized (this) {
            BeanDefinition definition = _scopeDefinition.declareNewDefinition(clazz,
                    instance, qualifier, secondaryTypes, override);
            instanceRegistry.saveDefinition(definition, true);
        }
    }

    public <T> void declare(Class<T> clazz,
                            T instance,
                            Qualifier qualifier) throws DefinitionOverrideException {
        this.declare(clazz, instance, qualifier, null, false);
    }

    /**
     * Get current Koin instance
     */
    public Koin getKoin() {
        return _koin;
    }

    /**
     * Get Scope
     *
     * @param scopeID
     */
    public Scope getScope(String scopeID) throws ScopeNotCreatedException {
        return getKoin().getScope(scopeID);
    }

    /**
     * Register a callback for this Scope Instance
     */
    public void registerCallback(ScopeCallback callback) {
        _callbacks.add(callback);
    }

    /**
     * Get a all instance for given inferred class (in primary or secondary type)
     *
     * @return list of instances of type T
     */
    //public <T> List<T> getAll(Class<T> clazz) {
    //    return getAll(clazz);
    //}

    /**
     * Get a all instance for given class (in primary or secondary type)
     *
     * @param clazz T
     * @return list of instances of type T
     */
    public <T> List<T> getAll(Class clazz) {
        List<T> result = new ArrayList<>();
        result.addAll(instanceRegistry.getAll(clazz));
        linkedScopes.forEach(new Consumer<Scope>() {
            @Override
            public void accept(Scope scope) {
                result.addAll(scope.getAll(clazz));
            }
        });
        return result;
    }

    /**
     * Get instance of primary type P and secondary type S
     * (not for scoped instances)
     *
     * @return instance of type S
     */
    public <S, P> S bind(
            Class<S> primaryType,
            Class<P> secondaryType,
            ParametersDefinition parameters
    ) throws NoBeanDefFoundException {
        S bind = instanceRegistry.bind(primaryType, secondaryType, parameters);
        if (bind != null) {
            return bind;
        } else {
            throw new NoBeanDefFoundException(
                    "No definition found to bind class:'"
                            + ClassUtil.getFullName(primaryType) + "' & secondary type:'"
                            + ClassUtil.getFullName(secondaryType) + "'. Check your definitions!"
            );
        }
    }

    /**
     * Retrieve a property
     *
     * @param key
     * @param defaultValue
     */
    public String getProperty(String key, String defaultValue) {
        return _koin.getProperty(key, defaultValue);
    }

    /**
     * Retrieve a property
     *
     * @param key
     */
    public String getProperty(String key) throws MissingPropertyException {
        String property = _koin.getProperty(key);
        if (property == null) {
            throw new MissingPropertyException("Property '" + key + "' not found");
        }
        return property;
    }

    /**
     * Retrieve a property
     *
     * @param key
     */
    public String getPropertyOrNull(String key) {
        return _koin.getProperty(key);
    }

    public void close() {
        synchronized (this) {
            clear();
            _koin.getScopeRegistry().deleteScope(this);
        }
    }

    public void clear() {
        _closed = true;
        _source = null;
        if (_koin.getLogger().isAt(Level.DEBUG)) {
            _koin.getLogger().info("closing scope:'" + id + "'");
        }
        // call on close from callbacks
        _callbacks.forEach(new Consumer<ScopeCallback>() {
            @Override
            public void accept(ScopeCallback callback) {
                callback.onScopeClose(Scope.this);
            }
        });
        _callbacks.clear();

        instanceRegistry.close();
    }

    @KoinInternalApi
    public BeanDefinition getBeanDefinition(Class clazz) {
        HashSet<BeanDefinition> definitions = this._scopeDefinition.getDefinitions();
        for (BeanDefinition definition : definitions) {
            if (definition.getPrimaryType().equals(clazz)) {
                return definition;
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return "Scope{"
                + "id='" + id + '\''
                + '}';
    }

    public void dropInstance(BeanDefinition beanDefinition) {
        instanceRegistry.dropDefinition(beanDefinition);
    }

    public void loadDefinition(BeanDefinition beanDefinition) {
        instanceRegistry.createDefinition(beanDefinition);
    }

    public void addParameters(DefinitionParameters parameters) {
        _parameters = parameters;
    }

    public void clearParameters() {
        _parameters = null;
    }

    public boolean isNotClosed() {
        return !getClosed();
    }

    public boolean getClosed() {
        return _closed;
    }

    public ScopeDefinition getScopeDefinition() {
        return _scopeDefinition;
    }

    public void setScopeDefinition(ScopeDefinition _scopeDefinition) {
        this._scopeDefinition = _scopeDefinition;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public InstanceRegistry getInstanceRegistry() {
        return instanceRegistry;
    }

    public Logger getLogger() {
        return logger;
    }
}
